#!/usr/bin/perl -w

#
# Copyright 2008 by Alex Linke, <alex@use-strict.de>
#
# Code reviews and documentation contributed by Rona Linke,
#   <rona@schnatterblog.de>
#
# $Id: swtk.pl 178 2008-12-05 18:11:49Z alex $
#
# Released under the terms of the BSD license, see "LICENSE" for details.
#

use strict;
use Getopt::Long;
use Text::Markdown  qw/markdown/;
use Text::Template;
use File::Find;
use File::Path;
use File::Basename;


my $VERSION = "0.1";


my $INSTALL_PREFIX = '%%PREFIX%%';     # replaced by sed


#
# Set some sane default values
#
my %opt = (
    EXT_INPUT        => ".swtk",
    EXT_OUTPUT       => ".html",
    DIR_INPUT        => "./swtk-data/",
    DIR_OUTPUT       => "./data/",
    TEMPLATE_DIR     => "./templates/",
    TEMPLATE_DEFAULT => "default.tmpl",
    SWTK_VERSION     => $VERSION,
    GENERATOR        => sprintf("swtk v%s/Perl v%vd", $VERSION, $^V),
);


#
# Parse commandline options first
#
my %cl_opt;

GetOptions(
    "c|config=s"           => \$cl_opt{config},
    "o|output-dir=s"       => \$cl_opt{dir_output},
    "i|input-dir=s"        => \$cl_opt{dir_input},
    "t|template-dir=s"     => \$cl_opt{template_dir},
    "d|template-default=s" => \$cl_opt{template_default},
    "e|ext=s"              => \$cl_opt{ext_input},
    "clean"                => \$cl_opt{clean},
    "s|sitemap"            => \$cl_opt{sitemap},
    "h|help"               => \$cl_opt{help},
    "v|verbose"            => \$cl_opt{verbose},
) or show_help(1);

show_help(0) if $cl_opt{help};


#
# A configuration file is mandatory, so inform the user about it and give
# hints on how to get started.
#
unless ($cl_opt{config})
{
    print STDERR
        "No configuration file was specified.\n\n",
        "Please read swtk's manpage and/or use the example configuration\n",
        "file as a template and suite it to your needs.\n\n",
        "It's located beneath $INSTALL_PREFIX/share/doc/swtk/example.\n";
    exit(1);
}


#
# Parse additional configuration from file, if switch was given, and merge
# the resulting parameters with the default ones
#
print "Reading configuration from $cl_opt{config}...\n";
my %config_opt = parse_config($cl_opt{config});

foreach my $key (keys %config_opt)
{
    $opt{uc($key)} = $config_opt{$key};
}


#
# Merge commandline line parameters. This way, commandline options
# always overwrite those read from a config file (or the default ones).
#
foreach my $key (keys %cl_opt)
{
    $opt{uc($key)} = $cl_opt{$key} if $cl_opt{$key};
}


#
# Check and fix file extensions if necessary
#
foreach my $ext (qw/EXT_INPUT EXT_OUTPUT/)
{
    $opt{$ext} = "." . $opt{ext} if ($opt{$ext} !~ /^\./);
}


#
# Find all input files
#
my @inputs;
die "No input directory defined!\n" unless defined $opt{DIR_INPUT};
die "$opt{DIR_INPUT}: $!!\n"        unless -d $opt{DIR_INPUT};

find(\&find_input_files, $opt{DIR_INPUT});

die "No input files found beneath $opt{DIR_INPUT} with extension " .
    "'$opt{EXT_INPUT}'.\n"
    unless scalar(@inputs);


#
# Should swtk clean up, only?
#
if ($opt{CLEAN})
{
    my $cnt      =   {dirs => 0, files => 0};

    #
    # Map all input files to their related output files
    #
    my @to_clean = map {s/^$opt{DIR_INPUT}/$opt{DIR_OUTPUT}\//; $_;} @inputs;

    #
    # Remove the sitemap as well if its generation is enabled
    #
    push @to_clean, "$opt{DIR_OUTPUT}/sitemap.xml" if $opt{SITEMAP};

    foreach my $path (@to_clean)
    {
        my $dir  = dirname($path) . "/";
        my $file = basename($path);

        $file    =~ s/$opt{EXT_INPUT}$/$opt{EXT_OUTPUT}/;

        if (-e "$dir/$file")
        {
            print "Removing $dir/$file...\n" if $opt{VERBOSE};

            unlink "$dir/$file" or die "$!\n";

            $cnt->{files}++;

            #
            # Remove all empty directories that were created in the file's
            # path until the output's root directory is reached.
            #
            while ($dir !~ /$opt{DIR_OUTPUT}\/*$/)
            {
                last unless is_empty_dir($dir);

                print "Removing empty dir $dir...\n" if $opt{VERBOSE};
                rmdir $dir or die "$!\n";

                $cnt->{dirs}++;
                $dir = dirname($dir) . "/";    # ..
            }
        }
    }

    print "Removed ", $cnt->{files}, " files and ", $cnt->{dirs},
        " directories.\n";

    exit(0);
}


#
# Process each input file
#
my $fc = 0;
foreach my $input (@inputs)
{
    $fc++;
    printf "%03d/%03d: %s\n", $fc, scalar(@inputs), $input if $opt{VERBOSE};

    process_swtk_file({%opt}, $input);
}

print "Processed $fc input files.\n";


#
# Generate a sitemap: sitemap.xml?
#
if ($opt{SITEMAP})
{
    my $cnt = 0;

    print "Generating sitemap.xml... " if $opt{VERBOSE};

    open SITEMAP, "> $opt{DIR_OUTPUT}/sitemap.xml"
        or die "$opt{DIR_OUTPUT}/sitemap.xml: $!\n";


    print SITEMAP "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";

    print SITEMAP "<?xml-stylesheet type=\"text/xsl\" ",
        "href=\"$opt{SITEMAP_STYLE}\"?>\n" if $opt{SITEMAP_STYLE};

    print SITEMAP
        "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";


    foreach my $file (@inputs)
    {
        my $url =  $file;
        $url    =~ s/$opt{EXT_INPUT}$/$opt{EXT_OUTPUT}/;
        $url    =~ s/^$opt{DIR_INPUT}/$opt{SITE_URL}/;
        $url    =~ s/(?<!http:)\/{2,}/\//g;

        next if ($opt{SITEMAP_IGNORE} && $url =~ /$opt{SITEMAP_IGNORE}/);

        $cnt++;

        print SITEMAP "\t<url><loc>$url</loc></url>\n";
    }

    print SITEMAP "</urlset>\n";

    print "$cnt entries.\n" if $opt{VERBOSE};
}


#
# This subroutine does the whole processing part in sequence:
#    * open the input file
#    * parse it
#    * process it (Text::Markdown)
#    * apply the template (Text::Template)
#    * determine the correct path and new filename
#    * store the output to disk
#
sub process_swtk_file
{
    my %opt    = %{shift()};
    my $input  = shift();
    my $text;


    open IN, $input or die "$input: $!\n";


    #
    # Try to read in additional parameters at the beginning of the file.
    # Parsing stops if the first attempt to read a key-value-pair fails.
    #
    foreach my $line (<IN>)
    {
        unless ($text)
        {
            if ($line =~ /^[\w_]+\s*=/)  # a variable initialization
            {
                my ($var, $val) = parse_config_line($line);

                $opt{$var} = $val;

                next;
            }
            else                         # first content line (ends parsing)
            {
                $text = $line;
            }
        }

        $text .= $line;
    }


    #
    # Compute the correct output directory and create it if necessary
    #
    my $dir_i = dirname($input) . "/";   # directory of this input file
    my $dir_o = $opt{DIR_OUTPUT};        # the user-defined output directory

    $dir_i    =~ s/$opt{DIR_INPUT}\/?//;
    $dir_o    .= ($dir_i =~ /^$opt{DIR_INPUT}$/) ? "/" : "/" . $dir_i;

    unless (-d $dir_o)
    {
        mkpath($dir_o) or die "Failed to create $dir_o: $!\n";
    }


    #
    # Compute the correct output filename
    #
    my $filename =  basename($input);
    $filename    =~ s/$opt{EXT_INPUT}$/$opt{EXT_OUTPUT}/;


    #
    # Compute the correct relative URL of this webpage
    #
    $opt{PAGE_URL} =  "/" . $dir_i . "/" . $filename;
    $opt{PAGE_URL} =~ s!/+!/!g;


    #
    # Apply markdown filter (Text::Markdown)
    #
    my $html =   Text::Markdown::markdown($text);


    #
    # Apply the template using Text::Template
    #
    my $tmpl =  $opt{TEMPLATE_DIR} . "/";
    $tmpl    .= (defined $opt{TEMPLATE}) ?
                $opt{TEMPLATE} : $opt{TEMPLATE_DEFAULT};

    die "No template defined.\n" unless $tmpl;
    die "$tmpl: not found.\n"    unless -e $tmpl;

    #
    # The two next commands were added by Christian Droulers <droulers.christian@gmail.com>
    # Allows the use of variables and templating in the .swtk files, instead of just in the .tmpl file.
    #
    my $innerTT = new Text::Template(
        type => "STRING",
        source => $html
    );    
    $html = $innerTT->fill_in(HASH => {
        %opt,
        FILENAME => $input
    });

    my $tt  = new Text::Template(
        type    => "FILE",
        source  => $tmpl,
    );
    my $output = $tt->fill_in(HASH => {
        %opt,
        FILENAME => $input,
        CONTENT  => $html,
    });
    die "Failed to apply template!\n" unless $output;


    #
    # Open the output file and write the webpage to disk
    #
    open OUT, "> $dir_o/$filename" or die "$dir_o/$filename: $!\n";

    print OUT $output;

    close OUT;
}


sub find_input_files  # passed to File::Find's find()
{
    push @inputs, $File::Find::name if ($_ =~ /$opt{EXT_INPUT}$/);
}


sub show_help
{
    print "$0 v$VERSION by Alex Linke <alex\@use-strict.de>\n\n";

    print
        " -c  --config           FILE     set mandatory configuration file\n",
        " -o  --output-dir       DIR      set output directory\n",
        " -i  --input-dir        DIR      set input directory\n",
        " -t  --template-dir     DIR      set template directory\n",
        " -d  --template-default FILE     set default template\n",
        " -e  --ext              EXT      set swtk file extension\n",
        " -s  --sitemap                   generate/clean sitemap as well\n",
        "     --clean                     clean up generated files\n",
        " -v  --verbose                   enable verbose output\n",
        " -h  --help                      show this help\n\n",
        "Have a look at swtk(1) for details.\n";

    exit($_[0] || 0);
}


sub is_empty_dir
{
    my $path = shift();

    opendir DIR, $path or die "$!\n";

    while (my $dir = readdir(DIR))
    {
        next if ($dir eq "." || $dir eq "..");

        closedir(DIR);

        return 0;
    }

    closedir(DIR);

    return 1;
}


sub parse_config
{
    my $config = shift();
    my %opt;

    open CONFIG, $config or die "$config: $!\n";

    foreach my $line (<CONFIG>)
    {
        next if ($line =~ /^\s*$/ || $line =~ /^#/);

        my ($var, $val) = parse_config_line($line);
    
        $opt{$var} = $val;
    }

    close CONFIG;

    return %opt;
}


#
# This sub parses a single config line to a variable and value pair.
#
# It assures that the quoting of values is correct and that every variable
# has a non-empty value.
#
sub parse_config_line {
    my $line = shift();

    chomp($line);

    $line =~ s/#.*$//;    # remove comments
    $line =~ s/\s+$//;    # remove trailing whitespace


    my ($var, $val) = split(/\s*=\s*/, $line, 2);


    die "Failed to parse line\n\t$line\nEmpty value.\n"
        if (length($val) == 0);

    #
    # Check for proper value quoting and remove it, if found
    #
    if ($val =~ /^(['"])/)
    {
        die "Failed to parse line\n\t$line\nCheck quoting...\n"
            unless ($val =~ /$1$/);

        $val =~ s/^["'](.*?)["']$/$1/;
    }

    die "Failed to parse line\n\t$line\nEmpty value.\n"
        if (length($val) == 0);
    
    return ($var, $val);
}


# vim: sts=4 enc=utf-8 ai expandtab
